import { Logger } from '@/types';
import { createLogger } from './logger';

/**
 * Retry configuration options
 */
export interface RetryOptions {
  maxAttempts: number;
  initialDelay: number;
  maxDelay: number;
  backoffFactor: number;
  jitter: boolean;
  retryCondition?: (error: Error) => boolean;
  onRetry?: (error: Error, attempt: number) => void;
}

/**
 * Default retry options
 */
const DEFAULT_RETRY_OPTIONS: RetryOptions = {
  maxAttempts: 3,
  initialDelay: 1000,
  maxDelay: 30000,
  backoffFactor: 2,
  jitter: true,
  retryCondition: () => true,
};

/**
 * Retry utility class for implementing retry logic
 */
export class RetryUtil {
  private readonly options: RetryOptions;
  private readonly logger: Logger;

  constructor(options: Partial<RetryOptions> = {}, logger?: Logger) {
    this.options = { ...DEFAULT_RETRY_OPTIONS, ...options };
    this.logger = logger ?? createLogger('RetryUtil');
  }

  /**
   * Execute a function with retry logic
   */
  async execute<T>(fn: () => Promise<T>): Promise<T> {
    let lastError: Error;

    for (let attempt = 1; attempt <= this.options.maxAttempts; attempt++) {
      try {
        return await fn();
      } catch (error) {
        lastError = error as Error;

        // Check if we should retry
        if (attempt === this.options.maxAttempts || !this.options.retryCondition?.(lastError)) {
          throw lastError;
        }

        // Calculate delay
        const delay = this.calculateDelay(attempt);

        // Call retry callback
        this.options.onRetry?.(lastError, attempt);

        this.logger.warn(`Attempt ${attempt} failed, retrying in ${delay}ms`, {
          error: lastError.message,
          attempt,
          maxAttempts: this.options.maxAttempts,
        });

        // Wait before retry
        await this.sleep(delay);
      }
    }

    throw lastError!;
  }

  /**
   * Calculate delay for the given attempt
   */
  private calculateDelay(attempt: number): number {
    let delay = this.options.initialDelay * Math.pow(this.options.backoffFactor, attempt - 1);

    // Apply max delay limit
    delay = Math.min(delay, this.options.maxDelay);

    // Apply jitter if enabled
    if (this.options.jitter) {
      delay = delay * (0.5 + Math.random() * 0.5);
    }

    return Math.floor(delay);
  }

  /**
   * Sleep for the specified duration
   */
  private sleep(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

/**
 * Circuit breaker states
 */
export enum CircuitBreakerState {
  CLOSED = 'closed',
  OPEN = 'open',
  HALF_OPEN = 'half_open',
}

/**
 * Circuit breaker configuration
 */
export interface CircuitBreakerOptions {
  failureThreshold: number;
  recoveryTimeout: number;
  monitoringPeriod: number;
  expectedErrors?: Array<new (...args: any[]) => Error>;
}

/**
 * Circuit breaker implementation
 */
export class CircuitBreaker {
  private state: CircuitBreakerState = CircuitBreakerState.CLOSED;
  private failureCount = 0;
  private lastFailureTime = 0;
  private readonly options: CircuitBreakerOptions;
  private readonly logger: Logger;

  constructor(options: Partial<CircuitBreakerOptions> = {}, logger?: Logger) {
    this.options = {
      failureThreshold: 5,
      recoveryTimeout: 60000, // 1 minute
      monitoringPeriod: 10000, // 10 seconds
      expectedErrors: [],
      ...options,
    };
    this.logger = logger ?? createLogger('CircuitBreaker');
  }

  /**
   * Execute a function with circuit breaker protection
   */
  async execute<T>(fn: () => Promise<T>): Promise<T> {
    if (this.state === CircuitBreakerState.OPEN) {
      if (Date.now() - this.lastFailureTime > this.options.recoveryTimeout) {
        this.state = CircuitBreakerState.HALF_OPEN;
        this.logger.info('Circuit breaker transitioning to half-open state');
      } else {
        throw new Error('Circuit breaker is open');
      }
    }

    try {
      const result = await fn();

      if (this.state === CircuitBreakerState.HALF_OPEN) {
        this.reset();
      }

      return result;
    } catch (error) {
      this.recordFailure(error as Error);
      throw error;
    }
  }

  /**
   * Record a failure
   */
  private recordFailure(error: Error): void {
    // Check if this is an expected error that shouldn't count towards circuit breaking
    if (this.options.expectedErrors?.some(ErrorClass => error instanceof ErrorClass)) {
      return;
    }

    this.failureCount++;
    this.lastFailureTime = Date.now();

    if (this.failureCount >= this.options.failureThreshold) {
      this.state = CircuitBreakerState.OPEN;
      this.logger.warn(`Circuit breaker opened after ${this.failureCount} failures`);
    }
  }

  /**
   * Reset the circuit breaker
   */
  private reset(): void {
    this.state = CircuitBreakerState.CLOSED;
    this.failureCount = 0;
    this.lastFailureTime = 0;
    this.logger.info('Circuit breaker reset to closed state');
  }

  /**
   * Get current state
   */
  getState(): CircuitBreakerState {
    return this.state;
  }

  /**
   * Get failure count
   */
  getFailureCount(): number {
    return this.failureCount;
  }
}

/**
 * Utility functions for retry operations
 */

/**
 * Retry a function with exponential backoff
 */
export async function retryWithBackoff<T>(
  fn: () => Promise<T>,
  options: Partial<RetryOptions> = {}
): Promise<T> {
  const retryUtil = new RetryUtil(options);
  return retryUtil.execute(fn);
}

/**
 * Create a circuit breaker
 */
export function createCircuitBreaker(
  options: Partial<CircuitBreakerOptions> = {},
  logger?: Logger
): CircuitBreaker {
  return new CircuitBreaker(options, logger);
}

/**
 * Retry decorator for methods
 */
export function retry(options: Partial<RetryOptions> = {}) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    const retryUtil = new RetryUtil(options);

    descriptor.value = async function (...args: any[]) {
      return retryUtil.execute(() => originalMethod.apply(this, args));
    };

    return descriptor;
  };
}

/**
 * Circuit breaker decorator for methods
 */
export function circuitBreaker(options: Partial<CircuitBreakerOptions> = {}) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    const breaker = new CircuitBreaker(options);

    descriptor.value = async function (...args: any[]) {
      return breaker.execute(() => originalMethod.apply(this, args));
    };

    return descriptor;
  };
}
